/*
 * Copyright (c) 2015, the DeltaSync# project. All Rights Reserved.
 * This project is a C# port of the JDeltaSync project:
 *     http://jdeltasync.googlecode.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
using System;
using System.IO;

namespace PM.Net.DeltaSync.hu01 {
    /**
     * This class provides an implementation of {@code FilterInputStream} that
     * uncompresses data that was compressed using the proprietary <i>HU01</i> algorithm
     * (see <a href="http://en.wikipedia.org/wiki/DeltaSync">Wikipedia's DeltaSync page</a>
     * for info on the compression). Basically it wraps a {@code HU01Decompressor} class 
     * and takes care of the buffering.
     * <p>
     * The source for this class was copied from Apache Harmony's {@code java.util.zip.InflaterInputStream}
     * and modified to use {@link HU01Decompressor} instead of {@link Inflater}.
     *
     * @see HU01Decompressor
     */
    public class HU01DecompressorStream : Stream {
        private static int BUF_SIZE = 512;

        /**
         * The input stream used in decompression.
         */
        private Stream input;
        private bool ownStream;
        
        /**
         * The decompressor used for this stream.
         */
        private HU01Decompressor decompressor;
    
        /**
         * The input buffer used for decompression.
         */
        private byte[] buf;
    
        private bool closed;
        private bool error;
    
        /**
         * This is the most basic constructor. You only need to pass the {@code
         * InputStream} from which the compressed data is to be read from. Default
         * settings for the {@code HU01Decompressor} and internal buffer are be used.
         *
         * @param is
         *            the {@code InputStream} to read data from.
         */
        public HU01DecompressorStream(Stream input, bool ownStream = true)
            : this(input, ownStream, new HU01Decompressor(), BUF_SIZE)
        {}
    
        /**
         * This constructor lets you pass a specifically initialized {@code HU01Decompressor}.
         *
         * @param is
         *            the {@code InputStream} to read data from.
         * @param decompressor
         *            the specific {@code HU01Decompressor} for uncompressing data.
         */
        public HU01DecompressorStream(Stream input, bool ownStream, HU01Decompressor decompressor)
            : this(input, ownStream, decompressor, BUF_SIZE)
        {}
    
        /**
         * This constructor lets you specify both the {@code HU01Decompressor} as well as
         * the internal buffer size to be used.
         *
         * @param is
         *            the {@code InputStream} to read data from.
         * @param decompressor
         *            the specific {@code HU01Decompressor} for uncompressing data.
         * @param bsize
         *            the size to be used for the internal buffer.
         */
        public HU01DecompressorStream(Stream input, bool ownStream, HU01Decompressor decompressor, int bsize) {
            if (input == null)
                throw new ArgumentNullException("InputStream");
            if (decompressor == null)
                throw new ArgumentNullException("Decompressor");
            if (bsize <= 0)
                throw new ArgumentException();
            this.input        = input;
            this.ownStream    = ownStream;
            this.decompressor = decompressor;
            this.error        = false;
            this.buf          = new byte[bsize];
        }
    
        /**
         * Called when stream is closed (or disposed).
         *
         * @throws IOException
         *             If an error occurs closing the input stream.
         */
        protected override void Dispose(bool disposing) {
            base.Dispose(disposing);
            if (!closed) {
                if (disposing && ownStream)
                    input.Close();
                closed = true;
                error  = false;
            }
        }
        
        public override bool CanRead {
            get { return true; }
        }
        public override bool CanSeek {
            get { return false; }
        }
        public override bool CanWrite {
            get { return false; }
        }
        
        public override long Position {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }
        public override long Length {
            get { throw new NotImplementedException(); }
        }
        public override void SetLength(long value) {
            throw new NotImplementedException();
        }
        public override long Seek(long offset, SeekOrigin origin) {
            throw new NotImplementedException();
        }
        
        /**
         * Reads up to {@code nbytes} of decompressed data and stores it in
         * {@code buffer} starting at {@code off}.
         *
         * @param buffer
         *            the buffer to write data to.
         * @param off
         *            offset in buffer to start writing.
         * @param nbytes
         *            number of bytes to read.
         * @return Number of uncompressed bytes read
         * @throws IOException
         *             if an IOException occurs.
         */
        public override int Read(byte[] buffer, int off, int nbytes) {
            if (buffer == null)
                throw new ArgumentNullException();
            if (off < 0 || nbytes < 0 || off + nbytes > buffer.Length)
                throw new ArgumentOutOfRangeException();
            if (closed)
                throw new ObjectDisposedException("Stream is closed");
            if (error)
                throw new InvalidOperationException("HU01 decompression stream is invalid and cannot be used anymore");
            
            if (nbytes == 0) {
                Fill();
                return 0;
            }
    
            try {
                do {
                    int result = decompressor.decompress(buffer, off, nbytes);
                    if (result > 0) 
                        return result;
                    
                    if (Fill() == 0) {
                        if (!decompressor.Finished)
                            throw new HU01Exception("HU01 stream truncated");
                        // end of stream reached...
                        return 0;
                    }
                } while (true);
            } catch (HU01Exception) {
                error = true;
                throw; // new IOException(ex.Message, ex);
            }
        }
    
        /**
         * Fills the input buffer with data to be decompressed.
         *
         * @throws IOException
         *             if an {@code IOException} occurs.
         */
        protected int Fill() {
            if (closed)
                throw new ObjectDisposedException("Stream is closed");
            
            int len;
            if ((len = this.input.Read(buf, 0, buf.Length)) > 0)
                decompressor.addInput(buf, 0, len);
            return len;
        }
    
        public override void Write(byte[] buffer, int off, int nbytes) {
            throw new NotSupportedException();
        }
        
        public override void Flush() {
            throw new NotImplementedException();
        }
    }
}